package org.calzz.jajb.io;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;

/**
 * The default implementation of the <code>IReader</code> interface. The only
 * method required consumes a <code>String</code> object and returns JSON
 * object in a form of a recursive <code>Map</code> which is later to be used
 * by the deserializer. Note that this implementation is meant to provide a high
 * performance and therefore contains no validation/grammar for the JSON input.
 * @see org.calzz.jajb.io.IReader
 * 
 * @author Marek Brodziak <marek.brodziak@gmail.com>
 * @author Piotr Pejas <piotr.pejas@gmail.com>
 * @since Feb 10, 2008
 */
public class JsonReader implements IReader {

	private static final String COLON = ":";
	private static final String COMMA = ",";
	private static final String ARRAY_START = "[";
	private static final String ARRAY_END = "]";
	private static final String OBJECT_START = "{";
	private static final String OBJECT_END = "}";
	private static final String DELIMITERS = "[]{},:";
	private static final String WHITESPACES = " \n\t";

	private Stack<Object> env = new Stack<Object>();
	private Map<String, Object> retValue;
	private String json;

	private String current_key;
	private int current_index;

	/*
	 * (non-Javadoc)
	 * @see org.jamjam.jajb.io.IReader#deserialize(java.lang.String)
	 */
	@SuppressWarnings("unchecked")
	public Map<String, Object> deserialize(String json) {
		current_index = 0;
		current_key = null;
		this.json = json.trim();

		String token;
		while ((token = nextToken()) != null) {

			token = token.trim();
			if (token.length() == 0) {
				continue;
			}

			if (OBJECT_START.equals(token)) {
				registerValue(new TreeMap<String, Object>());
			} else if (ARRAY_START.equals(token)) {
				registerValue(new LinkedList<Object>());
			} else if (OBJECT_END.equals(token)) {
				retValue = (Map<String, Object>) env.pop();
			} else if (ARRAY_END.equals(token)) {
				env.pop();
			} else if (COMMA.equals(token)) {
				// ignore
			} else if (COLON.equals(token)) {
				// ignore
			} else if (current_key == null && isObject()) {
				current_key = token;
			} else {
				registerValue(token);
			}
		}

		return retValue;
	}

	/**
	 * Registers a new value. If it's an array or an objects new section on the
	 * environment stack is opened. All primitive types are added to the current
	 * section by the current key taken from the input.
	 * @param obj new value to be registered
	 */
	@SuppressWarnings("unchecked")
	private void registerValue(Object obj) {
		if (isArray()) {
			((List) env.peek()).add(obj);
		} else if (isObject()) {
			((Map) env.peek()).put(current_key, obj);
		}

		if (obj instanceof List || obj instanceof Map) {
			env.push(obj);
		}

		current_key = null;
	}

	/**
	 * @return next token from the input
	 */
	private String nextToken() {
		if (current_index == json.length())
			return null;

		while (WHITESPACES.contains(String.valueOf(json.charAt(current_index))))
			current_index++;

		char c = json.charAt(current_index);
		if (c == '"') {
			return consumeString();
		} else if ((c >= '0' && c <= '9') || (c == '-') || (c == '.')) {
			return consumeNumber();
		} else if (c == 't' || c == 'f' || c == 'n') {
			return consumeNative();
		} else {
			current_index++;
			return String.valueOf(c);
		}
	}

	/**
	 * @return true, if the current context is an array
	 */
	@SuppressWarnings("unchecked")
	private boolean isArray() {
		return !env.isEmpty() && env.peek() instanceof List;
	}

	/**
	 * @return true, if the current context is an object
	 */
	@SuppressWarnings("unchecked")
	private boolean isObject() {
		return !env.isEmpty() && env.peek() instanceof Map;
	}

	/**
	 * @return JSON native string from the input
	 */
	private String consumeNative() {
		StringBuilder buffer = new StringBuilder();
		boolean consume = true;
		char c = json.charAt(current_index);
		while (consume) {
			buffer.append(c);
			current_index++;
			c = json.charAt(current_index);
			consume = !DELIMITERS.contains(String.valueOf(c));
		}
		return buffer.toString().trim();
	}

	/**
	 * @return number from the input
	 */
	private String consumeNumber() {
		StringBuilder buffer = new StringBuilder();
		boolean number = true;
		char c = json.charAt(current_index);
		while (number) {
			buffer.append(c);
			current_index++;
			c = json.charAt(current_index);
			number = (c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-' || c == 'E' || c == 'e';
		}
		return buffer.toString().trim();
	}

	/**
	 * @return <code>String</code> token from the input
	 */
	private String consumeString() {
		StringBuilder buffer = new StringBuilder();
		boolean escaped = false;
		current_index++;
		char c = json.charAt(current_index);
		while (true) {
			if (c == '"' && !escaped) {
				current_index++;
				break;
			}
			buffer.append(c);
			current_index++;
			c = json.charAt(current_index);
			escaped = c == '\\';
		}
		return buffer.toString().trim();
	}

}
